Welcome to the Developer's Guide for the SuiteQL Query Tool. This guide helps you understand the codebase architecture and learn SuiteScript development through a real-world, production-quality application.
suiteql-query-tool.v2026.1.suitelet.jsWant to see something working right away? Here's the fastest path to understanding how this tool works.
Open the SuiteQL Query Tool and paste this simple query into the editor:
SELECT
ID,
CompanyName,
Email
FROM
Customer
WHERE
ROWNUM <= 10
Click Run (or press Ctrl+Enter). You should see 10 customer records appear in the results.
Here's what just occurred behind the scenes:
handlePostRequest() function received it Line ~410executeQuery() Line ~660 which used query.runSuiteQL() to execute your SQLOpen suiteql-query-tool.v2026.1.suitelet.js and search for:
SECTION 4: QUERY EXECUTION Line ~651 - Where the query runs on the serverquery.runSuiteQL - The actual NetSuite API callfunction runQuery ~Line 7500 - The client-side JavaScript that sends the requestNow experiment! Try these modifications:
Customer to Vendor or EmployeePhone, DateCreatedROWNUM <= 10 to ROWNUM <= 25AND IsInactive = 'F'This quick reference table helps you find key sections and functions in the production script. Keep this handy as you explore the code.
// SECTION 4:) and function names to locate code if line numbers don't match exactly.
| Section | Description | Lines |
|---|---|---|
| Section 1: Configuration | CONFIG object, app settings, AI_ENABLED flag, PLUGIN_FOLDER_ID | 243-320 |
| Section 2: Module Definition | AMD define(), module imports | 321-360 |
| Section 3: Request Handlers | GET/POST routing, entry point, plugin handler dispatch | 361-450 |
| Section 3.5: Plugin System | Plugin loading, server hooks, settings persistence | 451-650 |
| Section 4: Query Execution | executeQuery(), pagination, plugin hooks (onBeforeQuery, onAfterQuery) | 651-780 |
| Section 5: File Operations | File Cabinet import/export | 781-910 |
| Section 6: Workbooks | NetSuite Workbooks integration | 911-960 |
| Section 6.5: AI Generation | All AI provider integrations (7 providers including OpenAI-Compatible) | 961-1650 |
| Section 7: Document Generation | PDF/HTML template rendering, Generate Suitelet feature | 1651-1850 |
| Section 7.5: Cloud Integrations | Google Sheets export with RSA-SHA256 JWT signing | 1851-2200 |
| Section 8: Tables Reference | Database schema browser | 2201-2250 |
| Section 9: Main App HTML | UI generation, CSS, modals, plugin injection points | 2251-5800 |
| Section 10: Client JavaScript | Browser-side app logic, SQT namespace, plugin client API, Chart.js integration | 5801-12000 |
| Section 11: Tables Reference HTML | Schema browser UI with AI integration | 12001-13500 |
| Section 12: Schema Explorer | Full schema export (12 formats), ERD generation with Mermaid.js | 13501-16500 |
| Function | Purpose | Line |
|---|---|---|
handleGetRequest() |
Renders the HTML UI | ~370 |
handlePostRequest() |
Routes POST requests to handlers | ~410 |
loadPlugins() |
Loads plugin files from File Cabinet | ~460 |
executePluginHook() |
Calls server-side plugin hooks | ~520 |
executeQuery() |
Runs SuiteQL queries with plugin hooks | ~660 |
importQueryFromFile() |
Loads SQL from File Cabinet | ~790 |
callAnthropicAPI() |
Anthropic Claude integration | ~1100 |
callOpenAIAPI() |
OpenAI GPT integration (also used by OpenAI-Compatible) | ~1250 |
callCohereAPI() |
Cohere Command integration | ~1400 |
generateDocument() |
PDF/HTML from templates | ~1660 |
generateSuitelet() |
Generate standalone Suitelet from template | ~1750 |
getGoogleSheetsToken() |
Google OAuth JWT token exchange (RSA-SHA256) | ~1900 |
createGoogleSpreadsheet() |
Google Sheets API - create spreadsheet | ~2000 |
appendToGoogleSheet() |
Google Sheets API - append data in batches | ~2050 |
generateAppHtml() |
Main UI HTML generation with plugin injection points | ~2260 |
generateTablesReferenceHtml() |
Tables browser UI | ~12010 |
generateSchemaExplorerHtml() |
Schema Explorer UI with 12 export formats | ~13510 |
| Function | Purpose | Approx. Line |
|---|---|---|
runQuery() |
Executes query via AJAX with plugin hooks | ~7500 |
displayResults() |
Renders results table with clickable record links | ~7800 |
exportToExcel() |
XLSX export via SheetJS | ~8500 |
formatSQL() |
SQL formatter with improved keyword detection | ~6800 |
callAI() |
Client-side AI call wrapper | ~9300 |
SQT.plugins.register() |
Register a client-side plugin | ~6200 |
SQT.plugins.executeHook() |
Execute plugin hooks (onInit, onBeforeQuery, etc.) | ~6250 |
showChart() |
Display Chart.js visualization modal | ~10500 |
generateRecordLink() |
Create clickable links to NetSuite records | ~7700 |
This Developer's Guide is designed to help you learn SuiteScript development by studying a real-world, production application. Rather than maintaining a separate "learning edition" with inline comments, this guide serves as a companion document you can reference alongside the production code.
Before diving into the code, here's a quick overview of SuiteScript fundamentals.
SuiteScript is NetSuite's server-side JavaScript API. It allows you to customize and extend NetSuite's functionality through scripts that run on NetSuite's servers. SuiteScript 2.x (the current version) uses the AMD module pattern and supports modern JavaScript features.
Every SuiteScript file starts with special JSDoc comments that tell NetSuite how to handle the script. Look at the top of the production script Lines 1-5:
/**
* @NApiVersion 2.1 // Use SuiteScript 2.1 (ES6+ JavaScript)
* @NScriptType Suitelet // This is a Suitelet (custom page/endpoint)
* @NModuleScope Public // Functions can be called from other scripts
*/
2.0 = SuiteScript 2.0 (ES5 JavaScript only)2.1 = SuiteScript 2.1 (ES6+ features: const/let, arrow functions, template literals)| Script Type | Purpose | Entry Point(s) |
|---|---|---|
| Suitelet | Custom pages and API endpoints | onRequest |
| User Event | Triggers on record events | beforeLoad, beforeSubmit, afterSubmit |
| Client Script | Browser-side form interactions | pageInit, saveRecord, fieldChanged, etc. |
| Scheduled | Background processing on a schedule | execute |
| Map/Reduce | Large-scale data processing | getInputData, map, reduce, summarize |
| RESTlet | REST API endpoints | get, post, put, delete |
SuiteScript functionality is organized into modules prefixed with "N/". You import these modules using the AMD define() function. See Lines 323-353 in the production script:
define([
'N/query', // SuiteQL queries
'N/file', // File Cabinet operations
'N/runtime' // Script/user/session info
], function(query, file, runtime) {
// Your code here
return {
onRequest: myFunction
};
});
Every SuiteScript operation consumes "governance units." Each script type has a limit:
| Script Type | Governance Limit |
|---|---|
| Suitelet | 1,000 units |
| User Event | 1,000 units |
| Client Script | 1,000 units |
| Scheduled Script | 10,000 units |
| Map/Reduce | 10,000 units per phase |
| RESTlet | 5,000 units |
query.runSuiteQL() = 10 unitsfile.load() = 10 unitsfile.save() = 10 unitshttps.post() = 10 unitsrecord.load() = 5-10 unitslog.debug() = 0 units
The SuiteQL Query Tool follows a client-server architecture where a single Suitelet serves both the HTML user interface and a JSON API.
+---------------------------------------------------------------------+
| USER'S BROWSER |
+---------------------------------------------------------------------+
| ^
| 1. GET request | 2. HTML page with
| (initial page load) | embedded JavaScript
v |
+---------------------------------------------------------------------+
| SUITELET |
| +-------------------------------------------------------------+ |
| | onRequest(context) | |
| | |-- GET --> handleGetRequest() --> serverWidget form | |
| | +-- POST --> handlePostRequest() --> JSON response | |
| +-------------------------------------------------------------+ |
+---------------------------------------------------------------------+
^ |
| 3. AJAX POST | 4. JSON data
| { function: 'queryExecute', ... } | { records: [...] }
| v
+---------------------------------------------------------------------+
| BROWSER JAVASCRIPT |
| +-----------------+ +-----------------+ +-----------------+ |
| | runQuery() | | loadSqlFile() | | callAI() | |
| | --> fetch() POST| | --> fetch() POST| | --> fetch() POST| |
| +-----------------+ +-----------------+ +-----------------+ |
+---------------------------------------------------------------------+
The Suitelet handles two types of requests (see Lines 354-430):
Instead of using NetSuite's built-in form widgets, we inject a complete custom HTML application using the INLINEHTML field type. This gives us full control over the user interface. See ~Line 1650.
The embedded JavaScript (starting at Line 5461) runs in the browser, not as SuiteScript. It uses standard web APIs like fetch() to communicate with the server. This is regular JavaScript with access to the DOM, localStorage, and third-party libraries.
POST requests include a function property that tells the server which operation to perform. The server uses a dispatch table to route requests (see ~Lines 410-430):
const handlers = {
'queryExecute': () => executeQuery(context, payload),
'sqlFileLoad': () => loadSqlFile(context, payload),
'sqlFileSave': () => saveSqlFile(context, payload),
'aiGenerate': () => generateAI(context, payload),
// ... more handlers
};
const handler = handlers[payload.function];
if (handler) handler();
The production script is organized into numbered sections. Here's what each section contains:
Object.freeze() for immutable config. Includes AI_ENABLED flag (Beta 06), PLUGIN_FOLDER_ID (Beta 09), ERD_CONFIG, and feature defaults.define() call, module imports (N/query, N/file, N/https, etc.), and module reference storage.handleGetRequest() and handlePostRequest() - the routing layer that dispatches to specific handlers, including plugin handler support.N/query, including pagination logic, virtual views support, and plugin hook integration.N/file - load, save, list files for SQL import/export.N/https to seven AI services: Anthropic, OpenAI, Cohere, xAI, Google, Mistral, plus OpenAI-Compatible for custom endpoints (Beta 09). API debug modal support (Beta 06).N/render and session storage with N/runtime. Includes "Generate Suitelet" feature (Beta 07) for creating standalone Suitelets from templates.These are the most instructive parts of the codebase for learning SuiteScript:
Location: Section 4, executeQuery() function ~Line 439
Learn how to execute SuiteQL queries and handle the results:
const records = modules.query.runSuiteQL({
query: 'SELECT ID, CompanyName FROM Customer WHERE IsInactive = ?',
params: ['F'] // Parameterized query - prevents SQL injection!
}).asMappedResults(); // Returns array of objects
Location: Section 5 Lines 564-691
Learn how to read and write files in NetSuite's File Cabinet:
// Loading a file
const fileObj = modules.file.load({ id: fileId });
const contents = fileObj.getContents();
// Creating a file
const newFile = modules.file.create({
name: 'query.sql',
contents: sqlText,
folder: folderId,
fileType: modules.file.Type.PLAINTEXT
});
const savedId = newFile.save();
Location: Section 6.5, AI provider functions Lines 737-1421
Learn how to make HTTPS requests to external services:
const response = modules.https.post({
url: 'https://api.anthropic.com/v1/messages',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify(requestBody)
});
if (response.code === 200) {
const data = JSON.parse(response.body);
}
Location: Section 7, document generation ~Line 1450
Learn how to persist data across requests within a user's session:
// Storing data
const session = modules.runtime.getCurrentSession();
session.set({
name: 'queryResults',
value: JSON.stringify(results)
});
// Retrieving data (in a later request)
const stored = session.get({ name: 'queryResults' });
const data = JSON.parse(stored);
Location: Section 7, generateDocument() ~Line 1450
Learn how to generate PDFs from templates with dynamic data:
const renderer = modules.render.create();
renderer.addCustomDataSource({
alias: 'data',
format: modules.render.DataSource.OBJECT,
data: { records: queryResults }
});
renderer.templateContent = xmlTemplate;
const pdfFile = renderer.renderAsPdf();
Location: Section 9, generateAppHtml() ~Line 2260
Learn the pattern for building single-page applications inside Suitelets using INLINEHTML, including client-server communication with fetch().
Location: Section 10 Lines 5801-12000
The client-side JavaScript is embedded inside a template literal. This requires special handling:
// comments)String.fromCharCode(96) for backtick charactersString.fromCharCode(36) for dollar signs in strings\\\\s to get \s)Location: Section 3.5 Lines 451-650 (server) and Section 10 ~Line 6200 (client)
Learn how to implement a hook-based extensibility system:
minAppVersion)Location: Section 10 ~Line 10500
Learn how to integrate external visualization libraries:
Location: generateRecordLink() ~Line 7700
Learn how to create intelligent links to NetSuite records based on column names and record types:
// Pattern: Detect ID columns and map to record types
const recordTypeMap = {
'customer': 'customer',
'entity': 'customer',
'vendor': 'vendor',
'item': 'item',
'transaction': 'transaction',
// ... more mappings
};
// Generate URL to record
const url = '/app/common/entity/entity.nl?id=' + recordId;
Location: Section 7.5 ~Lines 1900-2050
Pure JavaScript implementation of RSA-SHA256 for Google Service Account authentication. This is necessary because NetSuite's N/crypto module lacks RSA private key signing:
Location: CONFIG object ~Line 250
Learn how to implement feature toggles that cleanly enable/disable functionality:
const CONFIG = Object.freeze({
AI_ENABLED: true, // Master switch for AI features
PLUGIN_FOLDER_ID: null, // Enable plugin system
// ... other settings
});
When AI_ENABLED is false, all AI-related UI elements are hidden and server-side AI requests are rejected.
The SuiteQL Query Tool supports seven AI providers plus a custom "OpenAI-Compatible" option for any OpenAI-compatible API. Understanding this architecture helps you integrate external APIs into your own SuiteScript projects.
| Provider | Function | Line |
|---|---|---|
| Anthropic (Claude) | callAnthropicAPI() |
~1100 |
| OpenAI (GPT) | callOpenAIAPI() |
~1250 |
| Cohere (Command) | callCohereAPI() |
~1400 |
| xAI (Grok) | Uses OpenAI-compatible endpoint | ~1250 |
| Google (Gemini) | Uses OpenAI-compatible endpoint | ~1250 |
| Mistral AI | Uses OpenAI-compatible endpoint | ~1250 |
| OpenAI-Compatible (Custom) | Uses callOpenAIAPI() with custom base URL |
~1250 |
The "OpenAI-Compatible" option allows you to use any API that follows the OpenAI chat completions format. This includes:
Configuration requires a custom base URL and model name, making it flexible for enterprise deployments or self-hosted solutions.
All AI integrations follow a similar pattern:
N/httpscallAnthropicAPI() and callOpenAIAPI() to see how different APIs require different request formats but follow the same overall pattern.
The Schema Explorer (Lines 13501-16500) is a sophisticated tool that builds a complete picture of your NetSuite database schema.
The Schema Explorer now supports 12 export formats organized into "Common" and "Advanced" groups:
| Format | Use Case |
|---|---|
| JSON | Universal format for programmatic access |
| MySQL / PostgreSQL DDL | Replicate schema in relational databases |
| BigQuery / Snowflake DDL | Cloud data warehouse integration |
| Amazon Redshift DDL | AWS data warehouse |
| dbt YAML | dbt Core/Cloud source configuration |
| Apache Avro | Kafka/streaming data pipelines |
| Markdown | Documentation for GitHub, Confluence, Notion |
| DBML | dbdiagram.io ERD generation |
| Graphviz DOT | OmniGraffle, Graphviz tools |
The ERD viewer uses Mermaid.js to render entity-relationship diagrams. Key concepts:
Beta 09 introduced a comprehensive plugin system that allows developers to extend the SuiteQL Query Tool without modifying the core codebase. This is one of the most instructive parts of the application for learning extensibility patterns.
Plugins are self-contained JavaScript or JSON files stored in the NetSuite File Cabinet. The system supports:
Set CONFIG.PLUGIN_FOLDER_ID to the internal ID of a File Cabinet folder containing plugin files. Plugin files must be named with *.sqt-plugin.js or *.sqt-plugin.json extension.
| Hook | Purpose | Parameters |
|---|---|---|
onBeforeQuery |
Modify or cancel queries before execution | { query, params } |
onAfterQuery |
Process results after execution | { query, results, elapsed } |
onError |
Handle query errors | { query, error } |
| Hook | Purpose |
|---|---|
onInit |
Called when app initialization completes |
onBeforeQuery |
Modify or cancel queries from client |
onAfterQuery |
Process successful query results |
onResultsDisplay |
Customize result rendering |
onBeforeExport / onAfterExport |
Export lifecycle hooks |
onEditorChange |
Respond to editor changes |
toolbar-start, toolbar-end, more-dropdown, ai-dropdownheader-rightbefore-editor, editor-toolbar, nl-barresults-header, results-footer (dynamic via hooks)sidebar-sectionexport-menu, local-library-actions, modalsoptions-panel, status-bar// Plugin management
SQT.plugins.register(pluginConfig);
SQT.plugins.get('plugin-id');
SQT.plugins.executeHook('onAfterQuery', data);
// Data access
SQT.getResults(); // Get current query results
SQT.getQuery(); // Read editor content
SQT.setQuery(sql); // Write to editor
SQT.getEditor(); // Access CodeMirror instance
// UI control
SQT.showModal(id); // Show a Bootstrap modal
SQT.hideModal(id); // Hide a modal
A sample plugin (query-logger.sqt-plugin.js) is included that demonstrates:
PLUGIN-GUIDE.md for a complete plugin development guide with examples and best practices.
Beta 09 added data visualization capabilities powered by Chart.js, allowing users to create charts from query results.
Users can configure charts in two ways:
The Chart.js integration demonstrates how to:
// Example: Creating a chart from results
const ctx = document.getElementById('chartCanvas').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: results.map(r => r[labelColumn]),
datasets: [{
label: valueColumn,
data: results.map(r => r[valueColumn]),
backgroundColor: getThemeColors()
}]
}
});
Here's a recommended order for exploring the codebase:
query-logger.sqt-plugin.js) for practical examplesPLUGIN-GUIDE.md file for comprehensive documentation on creating your own plugins, including hook signatures, settings persistence, and UI injection patterns.
NetSuite has its own vocabulary. Here are key terms you'll encounter:
| Term | Definition |
|---|---|
| Internal ID | A unique numeric identifier assigned to every record in NetSuite. Used in scripts to reference specific records. |
| Script ID | A human-readable identifier you assign when creating scripts, custom fields, or custom records. Always prefixed (e.g., customscript_my_suitelet). |
| Deployment | An instance of a script configured to run. One script can have multiple deployments with different settings. |
| File Cabinet | NetSuite's file storage system. Organized into folders, stores scripts, images, documents. |
| Governance | NetSuite's resource management system. Each script operation costs "units" and scripts have limits. |
| Record Type | A category of data in NetSuite (e.g., Customer, Sales Order, Invoice). |
| Role | A set of permissions assigned to users. Determines what records and scripts a user can access. |
| Sandbox | A copy of your production NetSuite account for testing. |
| BUILTIN.DF() | "Display Field" - A SuiteQL function that returns the display name instead of the internal ID. |
| Hook | A function callback that plugins can register to intercept or modify application behavior at specific points (e.g., before query execution). |
| Injection Point | A predefined location in the UI where plugins can insert custom HTML elements. |
| IndexedDB | Browser-based database API used by Schema Explorer to persist schema data locally for fast access. |
| JWT | JSON Web Token - A compact, URL-safe token format used for authentication. The Google Sheets integration uses JWT with RSA-SHA256 signing. |
Here's how to deploy the SuiteQL Query Tool (or any Suitelet) to NetSuite:
Click the URL link on the deployment record to open the tool.
Solutions: Reduce operations, use getRemainingUsage() to monitor, or use Map/Reduce for large operations.
Solutions: Check column/table names in Analytics Browser, verify role permissions, column names are case-sensitive.
Solutions: Add restrictive WHERE clauses, remove unnecessary JOINs, avoid SELECT *, add date range filters.
| Table | Description |
|---|---|
Transaction | All transactions (orders, invoices) |
TransactionLine | Line items on transactions |
Customer | Customer records |
Vendor | Vendor records |
Employee | Employee records |
Item | All item types |
Account | Chart of accounts |
-- Get display value instead of internal ID
BUILTIN.DF( Transaction.status ) AS StatusName
-- Handle NULL values
NVL( Customer.phone, 'No Phone' ) AS Phone
-- Date formatting
TO_CHAR( Transaction.trandate, 'YYYY-MM-DD' ) AS FormattedDate
-- Current date
SYSDATE
-- Limit rows (no LIMIT keyword in SuiteQL!)
WHERE ROWNUM <= 100
// GOOD - Parameterized (safe)
query.runSuiteQL({
query: 'SELECT * FROM Customer WHERE id = ?',
params: [customerId]
});
// BAD - String concatenation (SQL injection risk!)
query.runSuiteQL({
query: 'SELECT * FROM Customer WHERE id = ' + customerId
});
Store keys in Script Parameters or have users enter them (stored in session).
Always validate data from external sources before using it.
log.debug({ title: 'Debug', details: myVariable });
log.error({ title: 'Error', details: e.message });
View logs: Script Deployment record → View Execution Log
For client-side JavaScript: Console, Network tab, Sources tab for breakpoints.
const remaining = runtime.getCurrentScript().getRemainingUsage();
log.debug('Governance remaining', remaining);
A: SuiteQL is based on Oracle SQL. Use ROWNUM instead: WHERE ROWNUM <= 10
A: Use Analytics Browser in NetSuite, the Tables Reference in this tool, or query OA_TABLES.
A: No. SuiteQL is read-only. Use the N/record module to modify data.
A: Use BUILTIN.DF(field) to get the display value instead of internal ID.
A: Set CONFIG.PLUGIN_FOLDER_ID to the internal ID of a File Cabinet folder. Place plugin files (*.sqt-plugin.js or *.sqt-plugin.json) in that folder.
A: Set CONFIG.AI_ENABLED to false. This hides all AI-related UI elements and rejects AI requests on the server.
A: Yes! Use the "OpenAI-Compatible" provider option to connect to any API that follows the OpenAI chat completions format (OpenRouter, Azure OpenAI, Ollama, etc.).
A: Click the "Chart" button in the results toolbar after running a query. Select chart type and configure the label/value columns, or describe what you want and let AI configure it.
A: The "Link IDs to records" option may be disabled in the Options panel. Enable it to automatically create clickable links for customer, vendor, employee, item, and transaction IDs.
Transaction, TransactionLine, Customer, Vendor, Employee, Item, Account
N/query, N/record, N/file, N/https, N/runtime, N/log, N/render
onBeforeQuery, onAfterQuery, onError
onInit, onBeforeQuery, onAfterQuery,onResultsDisplay, onEditorChange
BUILTIN.DF(field)
NVL(field, default)
TO_DATE('2024-01-01', 'YYYY-MM-DD')
SYSDATE
ROWNUM
SQT.getResults()
SQT.getQuery() / setQuery()
SQT.plugins.register(config)
SQT.showModal(id)